// -------------------------------------------------------------------------------------
// PaletteMatrix.m
// Martin D. Flynn, NeXT Computer, Inc.
// You may freely copy, distribute and reuse the code in this example.
// NeXT disclaims any warranty of any kind, expressed or implied, as to its
// fitness for any particular use.
// -------------------------------------------------------------------------------------

#import "PaletteMatrix.h"
#import <stdlib.h>
#import <stdio.h>
#import <string.h>
#import <mach/mach.h>
#import <objc/List.h>
#import <appkit/View.h>
#import <appkit/Text.h>
#import <appkit/NXImage.h>
#import <appkit/Pasteboard.h>
#import <appkit/FontManager.h>
#import <appkit/SavePanel.h>
#import <appkit/nextstd.h>
#import <dpsclient/wraps.h>
#import <math.h>
#import "ImagePortfolio.h"			// main app
#import "Portfolio.h"				// Matrix delegate
#import "PaletteCell.h"				// Image view cells

// -------------------------------------------------------------------------------------
static id		paletteCellClass = (id)nil;
static char		*cutListPaths   = (char*)nil;
static int		sigPasteboard   = -1;
static char		*imagePathType  = (char*)nil;
static id		pasteFont       = (id)nil;

// -------------------------------------------------------------------------------------
#define	prefVERSION	1.02
#define CELLCLASS	[PaletteCell class]
#define	newCELL		[protoCell copy]
#define	VSPACER		2.0
#define	freeLIST(L)	{ if (L) { [[L freeObjects] free]; L = (id)nil; } }

// -------------------------------------------------------------------------------------
@implementation PaletteMatrix

// -------------------------------------------------------------------------------------

/* new instance */
+ newFrame:(const NXRect *)fRect
{
	return [[self allocFromZone:NXDefaultMallocZone()] initFrame:fRect];
}

/* initialize instance */
- initFrame:(const NXRect*)fRect
{

	/* init globals */
	if (!imagePathType) imagePathType = NXCopyStringBuffer("imagePathType");

	/* init super */
	[super initFrame:fRect];
	if (!paletteCellClass) paletteCellClass = CELLCLASS;

	/* init vars */
	delegate = self;
	pbCopyTiff = YES;
	pbCopyPath = NO;
	loadingCells = NO;
	loadMutex = mutex_alloc();

	/* image list container */
	imageList = [[[List alloc] initCount:1] empty];
	deleteList = (id)nil;

	/* default protocell */
	protoCell = [paletteCellClass new];
	intercell.width  = VSPACER;
	intercell.height = VSPACER;
  
	/* size */
	[self setMode:NX_LISTMODE];
	[self setEmptySelectionEnabled:YES];
	[self setAutoscroll:YES];
	[self setBackgroundGray:NX_LTGRAY];
	[self setCellBackgroundGray:NX_LTGRAY];
	[self setFlipped:YES];

	return self;
}

/* free object */
- free
{
	[imageList free];		// cells are freed by super
	freeLIST(deleteList);
	mutex_free(loadMutex);
	return [super free];
}

/* free all cells */
- freeCells
{
	[self clearSelectedCell];
	[self renewRows:0 cols:0];
	[cellList freeObjects];
	[imageList empty];
	[self sizeToCells];
	freeLIST(deleteList);
	[window setDocEdited:YES];
	return self;
}

/* remove selected cells (return deleted list) */
- freeSelectedCells
{
	int	i;
	id	iCell, dList = [self selectedCellList];
  
	/* return if no selected cells */
	if (!dList) return (id)nil;
  
	/* delete selected cells from matrix */
	i = [dList count];
	[self clearSelectedCell];
	[self renewRows:0 cols:0];
	mutex_lock(loadMutex);
	while(i) { 
		iCell = [dList objectAt:--i];
		[imageList removeObject:iCell];
		[cellList removeObject:iCell];
		[iCell setDelegate:(id)nil];
	}
	mutex_unlock(loadMutex);
	[self sizeToCells];
	[window setDocEdited:YES];
  
	return dList;
}
  
// -------------------------------------------------------------------------------------
// set attributes
  
/* set delegate */
- setDelegate:anObject
{
	delegate = anObject;
	return self;
}

/* set to font */
- setFont:fontObj
{
	int i = [self cellCount];
	[super setFont:fontObj];
	[window disableDisplay];
	while(i) [[cellList objectAt:--i] setFont:fontObj];
	[protoCell setFont:fontObj];
	conFlags.calcSize = YES;
	[window reenableDisplay];
	return self;
}

- changeFont:sender
{
	[self setFont:[[FontManager new] convertFont:[self font]]];
	return self;
}

/* set preferences (THIS ASSUMES PROPER PREFERENCE BUFFER FORMAT) */
- (BOOL)setPreferences:(char*)prefBuff returnRows:(int*)rows cols:(int*)cols
{
	NXSize	size;
	float	ver, pointSize;
	char	fontName[256];
	id		o;
	if (!prefBuff || !*prefBuff) return NO;
	sscanf(prefBuff, "%f %f %f %d %d %s %f", &ver, 
		&size.width, &size.height, cols, rows, fontName, &pointSize);
	[self setCellSize:&size];
	if (o = [Font newFont:fontName size:pointSize]) [self setFont:o];
	return YES;
}
  
// -------------------------------------------------------------------------------------
// pasteboard

- (char*)imagePaths:list
{
	int		i, c, len = 0;
	char	*fullPath, *p;
	if (!(c = [list count])) return (char*)nil;
	for (i=0;i<c;i++) { len += strlen([[list objectAt:i] imagePath]) + 1; }
	fullPath = p = (char*)malloc(len + 1);
	for (i=0;i<c;i++) { sprintf(p, "%s\t", [[list objectAt:i] imagePath]); p += strlen(p); }
	*(p - 1) = 0;		// remove trailing tab (p > fullPath is guaranteed)
	return fullPath;
}

/* copy listed cells to pasteboard */
- copyToPasteboard:list paths:(char*)listPaths
{
	char		*dt[2], *data, *path;
	int			len, max, n = 0;
	NXStream	*stream;
	id			first, image, pb;
  
	/* return if no items to copy */
	if (![list count]) return self;
  
	/* return if no selected cell */
	pb = [Pasteboard newName:NXGeneralPboard];
	if (!pb) { NXLogError("ImagePortfolio: no pasteboard"); return self; }
  
	/* declare pasteboard types */
	if (pbCopyPath) dt[n++] = (char*)NXAsciiPboardType;
	if (pbCopyTiff) dt[n++] = (char*)NXTIFFPboardType;
	dt[n++] = (char*)imagePathType;
	[pb declareTypes:dt num:n owner:NXApp];
	sigPasteboard = [pb changeCount];
  
	/* get first image */
	first = [list objectAt:0];
  
	/* write image representation to pasteboard */
	if (pbCopyTiff) {
		stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
		image = [first image];
		[image setName:[first cellTitle]];
		[image lockFocus];
		[image writeTIFF:stream];
		[image unlockFocus];
		NXGetMemoryBuffer(stream, &data, &len, &max);
		[pb writeType:NXTIFFPboardType data:data length:len];
		NXClose(stream);
		NXFreeObjectBuffer(data, len);
	}
  
	/* write image path name to pasteboard */
	if (pbCopyPath && (path = (char*)[first imagePath])) {
		[pb writeType:NXAsciiPboardType data:path length:strlen(path) + 1];
	}

	/* write custom type€*/
	if (listPaths) [pb writeType:imagePathType data:listPaths length:strlen(listPaths) + 1];

	return self;
}

/* copy TIFF from pasteboard */
- copyFromPasteboard
{
	id			pb, pSave, imageId;
	char		**types, *data, titleBuff[256], *tiffName = (char*)nil;
	char		*path = (char*)[NXApp lastPath];
	int			len;
	NXStream	*stream;

	/* return if no pasteboard */
	if (!(pb = [Pasteboard newName:NXGeneralPboard])) return (id)nil;

	/* check for custom type */
	for (types = (char**)[pb types]; *types && strcmp(*types,imagePathType); types++);
	if (*types) {
		if (![pb readType:imagePathType data:&data length:&len]) return (id)nil;
		[delegate loadFileList:data :NO:NO];
		vm_deallocate(task_self(), (vm_address_t)data, (vm_size_t)len);
		return (id)nil;
	}
  
	/* search pasteboard types for a TIFF file */
	for (types=(char**)[pb types]; *types && strcmp(*types,(char*)NXTIFFPboardType); types++);
	if (!*types) return (id)nil;
  
	/* read data from pasteboard */
	if (![pb readType:NXTIFFPboardType data:&data length:&len]) return (id)nil;
  
	/* single-pass loop */
	for (;;) {
  
		/* load TIFF image */
		stream = NXOpenMemory(data, len, NX_READONLY);
		if (imageId = [NXImage alloc]) {
			NXSize size;
			[imageId setDataRetained:YES];
			[imageId initFromStream:stream];
			[imageId getSize:&size];
			if (!size.width || !size.height) { [imageId free]; imageId = (id)nil; }
		}
		if (!imageId) { NXCloseMemory(stream, NX_FREEBUFFER); break; }
	
		/* display image */
		sprintf(titleBuff, "%s - TIFF", ([imageId name]?[imageId name]:"NXImage"));
		[delegate showImage:imageId title:titleBuff];

		/* display save panel */
		pSave = [SavePanel new];
		[pSave setTitle:"Save TIFF file image"];
		[pSave setPrompt:"File:"];
		[pSave setRequiredFileType:"tiff"];
		[pSave setDirectory:path];
		if ([pSave runModalForDirectory:path file:""]) tiffName = (char*)[pSave filename];
  
		/* save to file */
		if (tiffName) NXSaveToFile(stream, tiffName);
		NXCloseMemory(stream, NX_FREEBUFFER);
		if (tiffName) [delegate loadFileList:tiffName :NO:NO];
  
		/* end single-pass loop */
		break;
	
	}
  
	/* free memory */
	vm_deallocate(task_self(), (vm_address_t)data, (vm_size_t)len);
	[delegate showImage:(id)nil title:""];  
	if (imageId) [imageId free];

	return self;
}
  
// -------------------------------------------------------------------------------------
// first responder
#define	delKEY		0x7F

/* never relinquish firstResponder */
- resignFirstResponder { return (id)nil; }

/* show preferences (menu connection should point here to allow disabling when necessary) */
- showPreferences:sender { return [NXApp showPreferences:self]; }

/* perform close window functions */
- performClose:sender { return [window performClose:sender]; }
- performMiniaturize:sender { return [window performMiniaturize:sender]; }

/* accept keydown event (check for delete key) */
- keyDown:(NXEvent*)e
{
	if (e->data.key.charCode == delKEY) return [self delete:(id)nil];
	return [super keyDown:e];
}

/* delete selected cells */
- delete:sender
{
	freeLIST(deleteList);
	deleteList = [self freeSelectedCells];
	[self display];
	return self;
}

/* copy previously deleted cells back to imageList */
- undelete:sender
{
	int	i, c;
	id	iCell;
	if (!deleteList) return self;
	for (c = [deleteList count], i = 0; i < c; i++) {
		iCell = [deleteList objectAt:i];
		if ([self findCellWithImageFilePath:(char*)[iCell imagePath]]) continue;
		[iCell setDelegate:delegate];
		[self addImageCell:iCell];
		[window setDocEdited:YES];
	}
	[deleteList free];
	deleteList = (id)nil;
	[self resizeAndDisplay];
	return self;
}

/* return true if undeletable */
- (BOOL)undeletable
{
	return ([deleteList count])? YES : NO;
}

/* cut selected cells */
- cut:sender
{
	[self delete:sender];
	if (!deleteList) return self;
	if (cutListPaths) { free(cutListPaths); cutListPaths = (char*)nil; }
	cutListPaths = [self imagePaths:deleteList];
	[self copyToPasteboard:deleteList paths:cutListPaths];
	return self;
}

/* make a copy of the selected cells */
- copy:sender
{
	id	dList = [self selectedCellList];
	if (!dList) return self;
	if (cutListPaths) { free(cutListPaths); cutListPaths = (char*)nil; }
	cutListPaths = [self imagePaths:dList];
	[self copyToPasteboard:dList paths:cutListPaths];
	[dList free];
	return self;
}

/* copy cut cells back to imageList */
- paste:sender
{
	if ([[Pasteboard new] changeCount] != sigPasteboard) [self copyFromPasteboard];
	else [delegate loadFileList:cutListPaths :NO:NO];
	[self resizeAndDisplay];
	return self;
}

/* select all cells */
- selectAll:sender
{
	int	i;
	if (mFlags.radioMode) return self;
	i = [self imageListCount];
	while(i) [[self imageAt:--i] setState:1];
	[self display];
	return self;
}

/* sort images by name */
- sortByCellTitle:sender
{
	[self shellSort:@selector(compareCellTitle::)];
	[self resizeAndDisplay];
	return self;
}

/* copy font */
- copyFont:sender
{
	pasteFont = [self font];
	return self;
}

/* paste font */
- pasteFont:sender
{
	if (pasteFont) [self setFont:pasteFont];
	return self;
}

// -------------------------------------------------------------------------------------
// first responder: save 

- (char*)getPreferenceString:(char*)buff
{
	int	rows, cols;
	[delegate getDisplayedRows:&rows cols:&cols];
	sprintf(buff, "%.2f %.0f %.0f %d %d %s %.0f", prefVERSION, 
		cellSize.width, cellSize.height, cols, rows, [font name], [font pointSize]);
	return buff;
}

/* save file names */
- save:sender   { return ([self imageListCount])? [delegate save:sender] : self; }
- saveAs:sender { return ([self imageListCount])? [delegate saveAs:sender] : self; }

/* save to file (called by delegate) */
- saveToFile:(char*)fileName
{
	int		i, c = [self imageListCount];
	char	buff[512];
	FILE	*fNum;
  
	/* get file name to save */
	if (!fileName || !c) return (id)nil;
  
	/* open file */
	fNum = fopen(fileName, "w");
	if (!fNum) return (id)nil;

	/* write preferences and path names */
	fprintf(fNum, ": %s\n", [self getPreferenceString:buff]); 
	for (i = 0; i < c; i++) fprintf(fNum, "%s\n", [[self imageAt:i] imagePath]);
  
	/* close file and indicate that window has been saved */
	fclose(fNum);
	[window setDocEdited:NO];

	return self;
}

// -------------------------------------------------------------------------------------
// size

/* set intercell size */
- setIntercell:(const NXSize*)aSize
{
	[super setIntercell:aSize];
	[self sizeToCells];
	return self;
}
  
/* set image size */
- setCellSize:(const NXSize*)size
{
	int	i = [self cellCount];

	/* update cell size */
	[super setCellSize:size];
  
	/* notify cells of size change */
	while(i) [[cellList objectAt:--i] setCellSize:size];
	[protoCell setCellSize:size];
  
	/* recalc sizes */
	[self sizeToCells];
  
	return self;
}

/* readjust matrix to fit cells */
- sizeToCells
{
	int		i, rows, cols, cnt;
	id		oldCell;

	/* clear selected cells */
	[self clearSelectedCell];
  
	/* adjust rows/cols */
	cnt = [self imageListCount];
	cols = (int)((frame.size.width + intercell.width) / (cellSize.width + intercell.width));
	if (cols < 1) cols = 1;
	rows = (cnt + cols - 1) / cols;
	if (rows < 1) rows = 1;
	[self renewRows:rows cols:cols];
  
	/* replace cells (and free old cells) */
	for (i = 0; i < cnt; i++) {
		oldCell = [cellList replaceObjectAt:i with:[self imageAt:i]];
		if (oldCell && ![oldCell image]) [oldCell free];
	}
  
	/* now resize matrix */
	[super sizeToCells];
  
	return self;
}

/* resize matrix and redisplay */
- resizeAndDisplay
{
	[self sizeToCells];
	[self display];
	return self;
}

/* return cellSize */
- (NXSize*)cellSize
{
	return &cellSize;
}

/* return intercellSize */
- (NXSize*)intercell
{
	return &intercell;
}

// -------------------------------------------------------------------------------------
// add cell methods

/* called by main thread when cell was added */
- _updateCells
{
	[self resizeAndDisplay];
	loadingCells = NO;
	return self;
}

/* add new image */
- addImage:(const char*)filePath
{
	id	iCell;
  
	/* see if its already in the list */
	if ([self findCellWithImageFilePath:(char*)filePath]) return (id)nil;
  
	/* add and show cell */
	iCell = newCELL;
	if (![iCell setImageFile:filePath]) { [iCell free]; return (id)nil; }
	[iCell setDelegate:delegate];
	[self addImageCell:iCell];
	if (!loadingCells) {
 	   loadingCells = YES;
 	   [self mainThreadPerform:@selector(_updateCells) wait:NO];
	}
  
	return iCell;
}

/* return current number of image cells */
- (int)imageListCount
{
	int	cnt;
	mutex_lock(loadMutex);
	cnt = [imageList count];
	mutex_unlock(loadMutex);
	return cnt;
}

/* return indexed image cell */
- imageAt:(int)index
{
	id	iCell;
	mutex_lock(loadMutex);
	iCell = [imageList objectAt:index];
	mutex_unlock(loadMutex);
	return iCell;
}

/* add image cell */
- addImageCell:iCell
{
	mutex_lock(loadMutex);
	[imageList addObject:iCell];
	mutex_unlock(loadMutex);
	return iCell;
}

// -------------------------------------------------------------------------------------
// cell selection

/* return cell at spcified mouse location */
- getCellAtLocation:(NXPoint*)mousePt
{
	float	x, y;
	float 	matWidth  = (numCols * (cellSize.width  + intercell.width )) - intercell.width ;
	float 	matHeight = (numRows * (cellSize.height + intercell.height)) - intercell.height;
	int		i, r = -1, c = -1;
	NXPoint	pt = *mousePt;
	
	/* convert location to matrix */
	[self convertPoint:&pt fromView:(id)nil];
	
	/* check location is within bounds */
	if ((pt.x < bounds.origin.x) || (pt.x > bounds.origin.x + matWidth ) ||
		(pt.y < bounds.origin.y) || (pt.y > bounds.origin.y + matHeight)   ) return (id)nil;
	
	/* find column */
	for (x=bounds.origin.x, i=0; (i<numCols) && (pt.x>=x); i++, x+=intercell.width) {
		x += cellSize.width;
		if (pt.x < x) { c = i; break; }
	}
	if (c < 0) return (id)nil;
	
	/* find row */
	for (y=bounds.origin.y, i=0; (i<numRows) && (pt.y>=y); i++, y+=intercell.height) {
		y += cellSize.height;
		if (pt.y < y) { r = i; break; }
	}
	if (r < 0) return (id)nil;

	/* return cell at location */
	return [self cellAt:r :c];
	
}

/* intercept mouse down event */
- mouseDown:(NXEvent*)e
{

	/* ignore while images are being loaded into palette */
	if ([delegate isLoading]) return self;
	
	/* check for Alternate-Shift pressed */
	if ((e->flags) & NX_ALTERNATEMASK) {
		NXPoint pt = e->location;
		id cellId = [self getCellAtLocation:&pt];
		NXRect rect = { { 0.0, 0.0 }, {48.0, 48.0 } };
		if (!cellId || ![cellId imagePath]) return self;
		[self convertPoint:&pt fromView:(id)nil];
		rect.origin.x = pt.x - rect.size.width  / 2.0;
		rect.origin.y = pt.y - rect.size.height / 2.0;
		[delegate _unregisterWindow];
		[self dragFile:[cellId imagePath] fromRect:&rect slideBack:YES event:e];
		[delegate _registerWindow];
		return self;
	}
	
	/* return regular mouse-down event */
	return [super mouseDown:e];
	
}

/* return true if there are any selectable cells */
- (BOOL)anySelectableCells
{
	return [self imageListCount]? YES : NO;
}

/* find image with name */
- findCellWithImageFilePath:(char*)p
{
	int	i;
	id	o, f = (id)nil;
	i = [self imageListCount];
	while(i) if (!strcmp([(o=[self imageAt:--i]) imagePath], p)) { f = o; break; }
	return f;
}

/* return list of selected cells */
- (int)selectedCellCount
{
	int	c = 0, i;
	i = [self imageListCount];
	while(i) if ([[self imageAt:--i] isSelected]) c++;
	return c;
}

/* return first selected cell found */
- selectedCell
{
	int	i;
	id	c, r = (id)nil;
	i = [self imageListCount];
	while(i) if ([(c = [self imageAt:--i]) isSelected]) r = c;
	return r;
}
  
/* return list of selected cells */
- selectedCellList
{
	int	i;
	id	c, list = listALLOC(2);
	i = [self imageListCount];
	while(i) if ([(c=[self imageAt:--i]) isSelected]) [list addObject:c];
	if (![list count]) { [list free]; list = (id)nil; }
	return list;
}

/* return string containing file names of all selected cells */
- (char*)selectedCellPaths
{
	id		list = [self selectedCellList];
	char	*fullPath = [self imagePaths:list];
	[list free];
	return fullPath;
}

// -------------------------------------------------------------------------------------
// sort compare methods

/* sort/compare by image name */
- (int)compareCellTitle:image1:image2
{
	return strcasecmp([image1 cellTitle], [image2 cellTitle]);
}

// -------------------------------------------------------------------------------------
// sort images

/* indexed from 1 */
#define	_COMPARE(I, J)	((int)[self perform:sortCompare with:(I) with:(J)])
#define	_ELEM(i)		[imageList objectAt:(i) - 1]
#define	_setELEM(i, V)	[imageList replaceObjectAt:(i) - 1 with:(V)]

/* shell sort: O(N ^ 1.5) */
- shellSort:(SEL)sortCompare
{
	int	i, j, h, N;
	id	elem, temp;
  
	/* check for invalid compare method */
	if (!sortCompare) return (id)nil;

	/* lock imageList */
	mutex_lock(loadMutex);

 	 /* determine h: (1 + 3 + 3^2 + 3^3 + 3^4 + ...) [ h = pow(3,floor(log(2*N+1)/log(3))) ]*/
	for (N = [imageList count], h = 1; h <= N; h = 3 * h + 1);
  
	/* shell sort */
	while (h > 1)
	for (h /= 3, i = h + 1; i <= N; i++) {
		elem = _ELEM(i);
		for (j=i; (j>h) && (_COMPARE((temp=_ELEM(j-h)),elem)>0); j-=h) _setELEM(j, temp);
		_setELEM(j, elem);
	}

	/* unlock imageList */
	mutex_unlock(loadMutex);
 
	return self;
}

@end
